package generate

import (
	"fmt"
	"path/filepath"
	"sort"
	"strings"
	"text/template"

	"google.golang.org/protobuf/compiler/protogen"
	"google.golang.org/protobuf/proto"

	"github.com/hashicorp/consul/internal/resource"
	"github.com/hashicorp/consul/proto-public/pbresource"
)

func Generate(gp *protogen.Plugin) error {
	g := newGenerator()

	for _, file := range gp.Files {
		err := g.addResourceKindsFromFile(file)
		if err != nil {
			return err
		}
	}

	return g.generateTypes(gp)
}

type apiGroupVersion struct {
	Group   string
	Version string
}

type groupInfo struct {
	Name    string
	Version string

	Kinds       []kind
	ImportPath  protogen.GoImportPath
	PackageName protogen.GoPackageName
	Directory   string
}

type kind struct {
	Name     string
	Comments protogen.CommentSet
}

type generator struct {
	resources map[apiGroupVersion]*groupInfo
}

func newGenerator() *generator {
	return &generator{
		resources: make(map[apiGroupVersion]*groupInfo),
	}
}

func (g *generator) addResourceKindsFromFile(f *protogen.File) error {
	if !f.Generate {
		return nil
	}

	for _, m := range f.Messages {
		ext := proto.GetExtension(m.Desc.Options(), pbresource.E_Spec).(*pbresource.ResourceTypeSpec)
		if ext == nil {
			continue
		}

		gvkString := strings.TrimPrefix(string(m.Desc.FullName()), "hashicorp.consul.")
		rtype, err := resource.ParseGVK(gvkString)
		if err != nil {
			return err
		}

		apiGroupDir := filepath.Dir(f.Proto.GetName())

		gv := apiGroupVersion{
			Group:   rtype.Group,
			Version: rtype.GroupVersion,
		}

		grp, err := g.ensureAPIGroup(gv, f.GoImportPath, f.GoPackageName, apiGroupDir)
		if err != nil {
			return err
		}

		grp.Kinds = append(grp.Kinds, kind{Name: rtype.Kind, Comments: m.Comments})
	}

	return nil
}

func (g *generator) ensureAPIGroup(gv apiGroupVersion, importPath protogen.GoImportPath, pkg protogen.GoPackageName, dir string) (*groupInfo, error) {
	grp, found := g.resources[gv]
	if !found {
		grp = &groupInfo{
			Name:    gv.Group,
			Version: gv.Version,

			ImportPath:  importPath,
			PackageName: pkg,
			Directory:   dir,
		}
		g.resources[gv] = grp
	} else if grp.ImportPath != importPath {
		return nil, fmt.Errorf("resources from the same api group must share the same import path")
	}

	return grp, nil
}

func (g *generator) generateTypes(gp *protogen.Plugin) error {
	for _, info := range g.resources {
		f := gp.NewGeneratedFile(filepath.Join(info.Directory, "resource_types.gen.go"), info.ImportPath)

		sort.Slice(info.Kinds, func(a, b int) bool {
			return info.Kinds[a].Name < info.Kinds[b].Name
		})

		if err := typesTemplate.Execute(f, info); err != nil {
			return err
		}
	}
	return nil
}

var (
	typesTemplate = template.Must(template.New("types").Parse(`
// Code generated by protoc-gen-resource-types. DO NOT EDIT.

package {{.PackageName}}

import (	
	"github.com/hashicorp/consul/proto-public/pbresource"
)

const (
	GroupName = "{{.Name}}"
	Version = "{{.Version}}"

{{range $kind := .Kinds}}
	{{$kind.Name}}Kind = "{{$kind.Name}}"
{{- end}}
)

var (
{{range $kind := .Kinds}}
	{{$kind.Name}}Type = &pbresource.Type{
 		Group:        GroupName,
 		GroupVersion: Version,
 		Kind:         {{$kind.Name}}Kind,
 	}
{{end}}	
)
`))
)
